package debtbundler.domain.bll.solver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import debtbundler.domain.entity.Debt;
import debtbundler.domain.entity.Person;

/**
 * A skeleton implementation for solvers. 
 * <p>
 * You need to implement the hook method <code>calculateSolution</code>.
 * Method <code>solve(Collection<Person></code> implements the basic logic:
 * <ol>
 * <li>Determine the <i>set</i> of persons (<code>allPersons</code>)</li>
 * <li>Test that the data is balanced or throw an exception if not</li>
 * <li>Determine who are the debtors, who are creditors, and who are debtless (sets <code>debtors</code>, <code>creditors</code>, <code>debtlessPersons</code> </li>
 * <li>Clear old debts</li>
 * <li>Call the<code>calculateSolution</code> hook method</li>
 * </ol>
 * 
 * @author Anssi
 *
 */
public abstract class SolverSkeleton {
	private static Logger logger = Logger.getLogger(SolverSkeleton.class.getName());
	
	protected LinkedHashSet<Person> allPersons = new LinkedHashSet<Person>();
	protected ArrayList<Person> debtors = new ArrayList<Person>();
	protected ArrayList<Person> creditors = new ArrayList<Person>();
	protected ArrayList<Person> debtlessPersons = new ArrayList<Person>();
	protected ArrayList<Debt> debts = new ArrayList<Debt>();
	protected boolean isReady = false;
	
	/**
	 * Calculates debts between the given persons.
	 * <p>
	 * Total balance, that is the sum of values, of payments and cost shares across
	 * all the persons must be close enough to zero.
	 * 
	 * @param persons	the persons whose debts to calculate
	 * @return			the calculated debts
	 * @throws UnbalancedCostsException 
	 * 	if payments and shares of the given persons are not balanced
	 * 	in necessary accuracy
	 */
	public List<Debt> solve(Collection<Person> persons) throws UnbalancedCostsException {
		allPersons.addAll(persons);
		testInputValidity();
		clearOldDebts();
		determinePersonRoles();
		calculateSolution();
		isReady = true;
		return getDebts();
	}

	/**
	 * Tests if the input data is balanced, i.e. sum of 
	 * values of payments and cost shares of across all
	 * persons is zero in accuracy of 0.005.
	 * 
	 * @throws UnbalancedCostsException
	 * 	if the input data is not balanced
	 */
	protected void testInputValidity() throws UnbalancedCostsException {
		
		double grandTotal = 0;
		for (Person person : allPersons) {
			grandTotal += person.getBalance();
		}
		if (Math.abs(grandTotal) >= 0.005) {
			logger.log(Level.WARNING, "Payments and cost shares of given persons are not balanced: {0}", grandTotal);
			throw new UnbalancedCostsException("Payments and cost shares of given persons are not balanced: " + grandTotal);
		}
		logger.log(Level.FINER, "Input data is valid");
	}
	
	/**
	 * Removes the current debts of all the input persons.
	 */
	protected void clearOldDebts() {
		for (Person person : allPersons) {
			LinkedList<Debt> toRemove = new LinkedList<Debt>();
			toRemove.addAll(person.getDebts());
			for (Debt debt : toRemove) {
				person.removeTransaction(debt);
			}
		}
		logger.log(Level.FINER, "Removed old debts");
	}
	
	/**
	 * Determines debtors, creditors, and debtless persons. 
	 */
	protected void determinePersonRoles() {
		for (Person person : allPersons) {
			if (person.getBalance() > 0) {
				debtors.add(person);
			} else if (person.getBalance() < 0) {
				creditors.add(person);
			} else {
				debtlessPersons.add(person);
			}
		}
		logger.log(Level.FINER, "Person roles determined.\nDebtors: {0}\nCreditors: {1}", new Object[]{debtors, creditors});
	}

	/**
	 * This is hook method where concrete classes should implement 
	 * their particular solver algorithms. The implementation 
	 * must create the debts in field <code>debts</code> for 
	 * <code>solve()</code> method to return.  
	 */
	protected abstract void calculateSolution(); 
	
	/**
	 * @return	the debts calculated by this solver
	 * @throws IllegalStateException
	 * 	if <code>solve(Collection)<code> has not been called
	 * @see solve(Collection) 			
	 */
	public List<Debt> getDebts() throws IllegalStateException {
		if (!isReady) {
			throw new IllegalStateException("Solve method has not been called");
		}
		return Collections.unmodifiableList(debts);
	}
}
